package com.cyj.springbootservice.utils;

import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.internal.OSSConstants;
import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.CompleteMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadRequest;
import com.aliyun.oss.model.InitiateMultipartUploadResult;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PartETag;
import com.aliyun.oss.model.StorageClass;
import com.aliyun.oss.model.UploadPartRequest;
import com.aliyun.oss.model.UploadPartResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @Author C_Y_J
 * @create 2022/5/18 17:20
 **/
public class AliyunOssUploadUtil {

    private final static Logger logger = LoggerFactory.getLogger(AliyunOssUploadUtil.class);
    private static String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
    private static String accessKeyId = "LTAI5t9Y95RL4BcFPAfzb6ev";
    private static String secretAccessKey = "TbvVyzCyY3MWi49iqlF6IyyIuj0QXG";
    private static String bucketName = "shard";

    public static void fileUploadOne(MultipartFile file) {
        LocalDateTime startLocalDateTime = LocalDateTime.now();
        long startTime = startLocalDateTime.toEpochSecond(ZoneOffset.of("+8"));
        logger.info("开始上传时间：{}", startLocalDateTime.toString());

        // 获取文件的原始名字，拼接成Object完整路径。Object完整路径中不能包含Bucket名称。
        String pack = startLocalDateTime.toLocalDate().toString();
        String originalFilename = file.getOriginalFilename();
        String objectName = pack + "/" + originalFilename;


        // 创建OSSClient实例。
        OSS ossClient = new OSSClient(endpoint, accessKeyId, secretAccessKey);

        try {

            // 创建InitiateMultipartUploadRequest对象。
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);

            // 如果需要上传时设置存储类型和访问权限，请参考以下示例代码。
            ObjectMetadata metadata = getObjectMetadata(originalFilename);
            request.setObjectMetadata(metadata);

            // 初始化分片。
            InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
            // 返回uploadId，它是分片上传事件的唯一标识，可以根据这个uploadId发起相关的操作，如取消分片上传、查询分片上传等。
            String uploadId = upresult.getUploadId();

            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
            List<PartETag> partETags = new ArrayList<PartETag>();
            //  每个分片的大小，用于计算文件有多少个分片。单位为字节。
            final long partSize = 1024 * 1024L;
            long fileLength = file.getSize();
            // partCount：需要分片的次数
            int partCount = (int) (fileLength / partSize);
            if (fileLength % partSize != 0) {
                partCount++;
            }

            // 遍历分片上传。
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                long curPartSize = (i + 1 == partCount) ? (fileLength - startPos) : partSize;

                InputStream inputStream = file.getInputStream();
                // 跳过已经上传的分片。
                inputStream.skip(startPos);

                UploadPartRequest uploadPartRequest = new UploadPartRequest();
                uploadPartRequest.setBucketName(bucketName);
                uploadPartRequest.setKey(objectName);
                uploadPartRequest.setUploadId(uploadId);
                uploadPartRequest.setInputStream(inputStream);
                // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
                uploadPartRequest.setPartSize(curPartSize);
                // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出这个范围，OSS将返回InvalidArgument的错误码。
                uploadPartRequest.setPartNumber(i + 1);
                // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
                UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                // 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
                partETags.add(uploadPartResult.getPartETag());
            }

            /**
             * 创建CompleteMultipartUploadRequest对象。
             * 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。
             * 当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
             */
            CompleteMultipartUploadRequest uploadRequest = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);

            // 如果需要在完成分片上传的同时设置文件访问权限，请参考以下示例代码。
            uploadRequest.setObjectACL(CannedAccessControlList.PublicRead);

            // 完成上传。
            ossClient.completeMultipartUpload(uploadRequest);
            // 关闭OSSClient。
            ossClient.shutdown();

            LocalDateTime endLocalDateTime = LocalDateTime.now();
            long endTime = endLocalDateTime.toEpochSecond(ZoneOffset.of("+8"));
            logger.info("开始上传时间：{}", endLocalDateTime.toString());
            logger.info("文件上传成功，耗时：{}秒", endTime - startTime);

        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            ossClient.shutdown();
        }
    }

    public static ObjectMetadata getObjectMetadata(String originalFilename) {
        // 创建上传Object的Metadata
        ObjectMetadata metadata = new ObjectMetadata();

        metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
        // 指定该Object的网页缓存行为。
        metadata.setCacheControl("no-cache");
        // 指定该Object被下载时的名称。
        metadata.setContentDisposition("attachment;filename=" + originalFilename);
        // 指定该Object的内容编码格式。
        metadata.setContentEncoding(OSSConstants.DEFAULT_CHARSET_NAME);
        // 指定初始化分片上传时是否覆盖同名Object。此处设置为true，表示禁止覆盖同名Object。
        metadata.setHeader("x-oss-forbid-overwrite", "true");
        // 指定上传该Object的每个part时使用的服务器端加密方式。
        metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_ENCRYPTION, ObjectMetadata.KMS_SERVER_SIDE_ENCRYPTION);
        // 指定Object的加密算法。如果未指定此选项，表明Object使用AES256加密算法。
        metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_DATA_ENCRYPTION, ObjectMetadata.KMS_SERVER_SIDE_ENCRYPTION);
        // 指定KMS托管的用户主密钥。
        metadata.setHeader(OSSHeaders.OSS_SERVER_SIDE_ENCRYPTION_KEY_ID, "9468da86-3509-4f8d-a61e-6eab1eac****");
        //指定Object的存储类型。
        metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard);
        // 指定Object的对象标签，可同时设置多个标签。
        metadata.setHeader(OSSHeaders.OSS_TAGGING, "a:1");
        return metadata;
    }

    /**
     * #########分割线##################分割线#########
     */

    public static void fileUploadTwo(MultipartFile file) {
        LocalDateTime startLocalDateTime = LocalDateTime.now();
        long startTime = startLocalDateTime.toEpochSecond(ZoneOffset.of("+8"));
        System.out.println("开始上传时间" + startLocalDateTime.toString());

        // 获取文件的原始名字，拼接成Object完整路径。Object完整路径中不能包含Bucket名称。
        String pack = startLocalDateTime.toLocalDate().toString();
        String originalFilename = file.getOriginalFilename();
        String objectName = pack + "/" + originalFilename;

        // 获取阿里云OSS客户端对象
        OSS ossClient = getOSSClient();

        // 创建一个可重用固定线程数的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        try {

            // 获取Object的Metadata
            long fileSize = file.getSize();
            ObjectMetadata metadata = getMetadata(originalFilename, fileSize);

            // 创建InitiateMultipartUploadRequest对象
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName, metadata);

            // 初始化分片
            InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
            // 返回uploadId，它是分片上传事件的唯一标识，您可以根据这个uploadId发起相关的操作，如取消分片上传、查询分片上传等
            String uploadId = upresult.getUploadId();

            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成
            List<PartETag> partETags = Collections.synchronizedList(new ArrayList<>());
            // 计算文件有多少个分片
            final long partSize = 1024 * 1024L;
            int partCount = (int) (fileSize / partSize);
            if (fileSize % partSize != 0) {
                partCount++;
            }
            if (partCount > 10000) {
                throw new RuntimeException("文件过大");
            }

            // 遍历分片上传
            for (int i = 0; i < partCount; i++) {
                long startPos = i * partSize;
                long curPartSize = (i + 1 == partCount) ? (fileSize - startPos) : partSize;
                int partNumber = i + 1;


                // 实现并启动线程
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        InputStream inputStream = null;
                        try {
                            inputStream = file.getInputStream();
                            // 跳过已经上传的分片
                            inputStream.skip(startPos);

                            UploadPartRequest uploadPartRequest = new UploadPartRequest();
                            uploadPartRequest.setBucketName(bucketName);
                            uploadPartRequest.setKey(objectName);
                            uploadPartRequest.setUploadId(uploadId);
                            uploadPartRequest.setInputStream(inputStream);
                            // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
                            uploadPartRequest.setPartSize(curPartSize);
                            // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出这个范围，OSS将返回InvalidArgument的错误码。
                            uploadPartRequest.setPartNumber(partNumber);
                            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
                            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                            //每次上传分片之后，OSS的返回结果会包含一个PartETag。PartETag将被保存到PartETags中。
                            synchronized (partETags) {
                                partETags.add(uploadPartResult.getPartETag());
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            if (inputStream != null) {
                                try {
                                    inputStream.close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                });
            }

            // 等待所有的分片完成
            // shutdown方法：通知各个任务（Runnable）的运行结束
            executorService.shutdown();
            while (!executorService.isTerminated()) {
                try {
                    // 指定的时间内所有的任务都结束的时候，返回true，反之返回false，返回false还有执行完的任务
                    executorService.awaitTermination(5, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    System.out.println(e.getMessage());
                }
            }

            // 验证是否所有的分片都完成
            if (partETags.size() != partCount) {
                throw new IllegalStateException("文件的某些部分上传失败！");
            }

            // 创建CompleteMultipartUploadRequest对象
            // 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件
            CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);

            // 设置文件访问权限
            completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.PublicRead);


            // 完成上传
            ossClient.completeMultipartUpload(completeMultipartUploadRequest);
            // 关闭OSSClient。
            ossClient.shutdown();

            LocalDateTime endLocalDateTime = LocalDateTime.now();
            long endTime = endLocalDateTime.toEpochSecond(ZoneOffset.of("+8"));
            System.out.println("开始上传时间" + endLocalDateTime.toString());
            System.out.println("文件上传成功，耗时：" + (endTime - startTime) + "秒");
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            // 关闭OSSClient
            ossClient.shutdown();
        }
    }

    /**
     * 获取阿里云OSS客户端对象
     *
     * @return ossClient
     */
    public static OSS getOSSClient() {
        ClientBuilderConfiguration conf = new ClientBuilderConfiguration();
        // 连接空闲超时时间，超时则关闭
        conf.setIdleConnectionTime(1000);
        return new OSSClientBuilder().build(endpoint, accessKeyId, secretAccessKey, conf);
    }

    /**
     * 设置上传对象的属性
     *
     * @param originalFilename
     * @param fileSize
     * @return
     */
    private static ObjectMetadata getMetadata(String originalFilename, long fileSize) {
        // 创建上传Object的Metadata
        ObjectMetadata metadata = new ObjectMetadata();
        // 指定该Object被下载时的网页的缓存行为
        metadata.setCacheControl("no-cache");
        // 指定该Object下设置Header
        metadata.setHeader("Pragma", "no-cache");
        // 指定该Object被下载时的内容编码格式
        metadata.setContentEncoding("utf-8");
        // 文件的MIME，定义文件的类型及网页编码，决定浏览器将以什么形式、什么编码读取文件。如果用户没有指定则根据Key或文件名的扩展名生成，
        // 如果没有扩展名则填默认值application/octet-stream
        metadata.setContentType(getContentType(originalFilename));
        // 指定该Object被下载时的名称（指示MINME用户代理如何显示附加的文件，打开或下载，及文件名称）
        metadata.setContentDisposition("filename/filesize=" + originalFilename + "/" + fileSize + "Byte.");
        return metadata;
    }

    /**
     * 通过文件名判断并获取OSS服务文件上传时文件的contentType
     *
     * @param fileExtension 文件名扩展名
     * @return 文件的contentType
     */
    public static String getContentType(String fileExtension) {
        // 文件的后缀名
        if (".bmp".equalsIgnoreCase(fileExtension)) {
            return "image/bmp";
        }
        if (".gif".equalsIgnoreCase(fileExtension)) {
            return "image/gif";
        }
        if (".jpeg".equalsIgnoreCase(fileExtension) || ".jpg".equalsIgnoreCase(fileExtension)
                || ".png".equalsIgnoreCase(fileExtension)) {
            return "image/jpeg";
        }
        if (".html".equalsIgnoreCase(fileExtension)) {
            return "text/html";
        }
        if (".txt".equalsIgnoreCase(fileExtension)) {
            return "text/plain";
        }
        if (".vsd".equalsIgnoreCase(fileExtension)) {
            return "application/vnd.visio";
        }
        if (".ppt".equalsIgnoreCase(fileExtension) || "pptx".equalsIgnoreCase(fileExtension)) {
            return "application/vnd.ms-powerpoint";
        }
        if (".doc".equalsIgnoreCase(fileExtension) || "docx".equalsIgnoreCase(fileExtension)) {
            return "application/msword";
        }
        if (".xml".equalsIgnoreCase(fileExtension)) {
            return "text/xml";
        }
        if (".mp4".equalsIgnoreCase(fileExtension)) {
            return "video/mp4";
        }
        // android
        if (".apk".equalsIgnoreCase(fileExtension)) {
            return "application/vnd.android.package-archive";
        }
        // ios
        if (".ipa".equals(fileExtension)) {
            return "application/vnd.iphone";
        }
        // 默认返回类型
        return "application/octet-stream";
    }

}
